﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using GalaSoft.MvvmLight.Messaging;
using SessionHunter.Business.Exceptions;
using SessionHunter.Messages;
using SessionHunter.Model;
using SessionHunter.Utilities;

namespace SessionHunter.DataAccess
{
    /// <summary>
    /// This is responsible for storing, retrieving and persisting the application options
    /// and connection string.  Maps data from the configuration file into objects.
    /// </summary>
    public class ApplicationRepository
    {
        private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

        #region Constants
        /// <summary>
        /// The name of the isolated storage to use
        /// </summary>
        private const string _isolatedStorageName = "SessionSettings.xml";
        /// <summary>
        /// The root element of the XML we are saving
        /// </summary>
        private const string _xDocumentRoot = "Settings";
        /// <summary>
        /// The element with the references
        /// </summary>
        private const string _xElementReferences = "References";
        /// <summary>
        /// The element for a single reference
        /// </summary>
        private const string _xElementReference = "Reference";
        #endregion

        #region Enums
        /// <summary>
        /// These are the items that are persisted
        /// </summary>
        public enum PersistedSettings
        {
            /// <summary>
            /// Setting for number of sessions
            /// </summary>
            NumberOfSessions,
            /// <summary>
            /// Setting for order of results
            /// </summary>
            OrderBy,
            /// <summary>
            /// Setting for what to do with the application
            /// </summary>
            Application,
            /// <summary>
            /// Setting for create/expires columns
            /// </summary>
            ShowCreateExpires,
            /// <summary>
            /// Setting for showing lock info columns
            /// </summary>
            ShowLockInfo
        }
        #endregion

        #region Variables
        /// <summary>
        /// Whether or not the settings have changed and need to be broadcast
        /// </summary>
        private static bool _isSettingsDirty = false;
        #endregion

        /// <summary>
        /// Default constructor
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        public ApplicationRepository()
        {
            StorageFileName = _isolatedStorageName;
            logger.Info("Loading ApplicationRepository for file: " + _isolatedStorageName);
        }

        /// <summary>
        /// Gets the options from persisted storage.
        /// </summary>
        /// <returns>The settins or a default set </returns>
        public ApplicationOptions GetOptions()
        {
            Contract.Ensures(Contract.Result<ApplicationOptions>() != null, "Cannot return a null value from GetOptions");

            // Start off with a "default" application option
            var appOption = new ApplicationOptions();
            bool tempBool;
            int tempInt;

            // Get the options from isolated storage and overwrite any settings we have actually saved before
            var settings = GetSettingsFromStorage(StorageFileName);
            if (settings != null)
            {
                // Take any settings and assign them the existing options
                foreach (var settingItem in settings.Root.Elements().Where(t => t.Name != _xElementReferences))
                {
                    PersistedSettings settingEnum;
                    // See if we can parse the value in the XML to  one of our existing settings.  If we can't parse it, ignore it because it may be a deprecated setting
                    if (Enum.TryParse<PersistedSettings>(settingItem.Name.ToString(), out settingEnum))
                    {
                        // Set each item.  If we can't parse it, just forget that one setting because we probably can't use it anyways.  It will fall back to the default
                        switch (settingEnum)
                        {
                            case PersistedSettings.Application:
                                if (bool.TryParse(settingItem.Value, out tempBool))
                                    appOption.Application = tempBool;
                                break;
                            case PersistedSettings.NumberOfSessions:
                                if (Int32.TryParse(settingItem.Value, out tempInt))
                                    appOption.NumberOfSessions = tempInt;
                                break;
                            case PersistedSettings.OrderBy:
                                ApplicationOptions.OrderByParameter tempParam_OrderBy;
                                if (Enum.TryParse(settingItem.Value, out tempParam_OrderBy))
                                    appOption.OrderBy = tempParam_OrderBy;
                                break;
                            case PersistedSettings.ShowCreateExpires:
                                if (bool.TryParse(settingItem.Value, out tempBool))
                                    appOption.ShowCreateExpires = tempBool;
                                break;
                            case PersistedSettings.ShowLockInfo:
                                if (bool.TryParse(settingItem.Value, out tempBool))
                                    appOption.ShowLockInfo = tempBool;
                                break;
                            default:
                                throw new UnhandledCaseException(settingEnum);
                        }
                    }
                }

                // Get all of the references we are going to pre-load
                var referenceRoot = settings.Root.Element(_xElementReferences);
                if (referenceRoot != null)
                {
                    foreach (var reference in settings.Root.Element(_xElementReferences).Elements())
                    {
                        var path = reference.Value;
                        if (!String.IsNullOrWhiteSpace(path))
                        {
                            if (appOption.ReferencePaths == null)
                                appOption.ReferencePaths = new List<string>();

                            appOption.ReferencePaths.Add(path);
                        }
                    }
                }
            }

            // Return the default + saved settings (if any)
            return appOption;
        }

        /// <summary>
        /// Saves the option to persisted storage.
        /// </summary>
        /// <typeparam name="T">The data type to save</typeparam>
        /// <param name="setting">The setting to save</param>
        /// <param name="valueToSet">The value of the item to save</param>
        public virtual void SaveOption<T>(PersistedSettings setting, T valueToSet)
        {
            Contract.Requires(valueToSet != null);

            string settingKey = Enum.GetName(typeof(PersistedSettings), setting);
            string settingValue = valueToSet.ToString();

            logger.Info("Save Option: " + settingKey + " to " + settingValue);

            if (String.IsNullOrEmpty(settingKey))
                throw new SessionApplicationException("Setting to set is null for SaveOption");
            if (String.IsNullOrEmpty(settingValue))
                throw new SessionApplicationException("Setting value to set is null for SaveOption");

            var settings = GetSettingsFromStorage(StorageFileName);

            // See if we have any settings saved
            if (settings == null)
            {
                // Create a new settings file if we don't have one
                settings = new XDocument(
                                new XElement(_xDocumentRoot, new XElement(settingKey, settingValue))
                                );
            }
            else
            {
                // See if we already have the setting           
                logger.Debug("Looking in document for " + settingKey);
                var xmlSetting = settings.Element(_xDocumentRoot).Element(settingKey);
                if (xmlSetting == null)
                {
                    logger.Debug("Save Option - Adding " + settingKey + " - " + settingValue);
                    // Add the single setting
                    settings.Root.AddFirst(new XElement(settingKey, settingValue));
                }
                else
                {
                    logger.Debug("Save Option - Overwriting " + settingKey + " - " + settingValue);
                    // Overwrite the setting
                    xmlSetting.Name = settingKey;
                    xmlSetting.Value = settingValue;
                }
            }

            // Save the existing or new settings
            SaveSettingsToStorage(settings);

            _isSettingsDirty = true;
        }

        /// <summary>
        /// This will take all of the current references and save them to the settings files.
        /// </summary>
        public virtual void SaveReferences(List<ExternalReference> references)
        {
            logger.Info("Update references in setting file");

            var settings = GetSettingsFromStorage(StorageFileName);

            // Create a new settings file if we don't have one
            if (settings == null)
            {
                settings = new XDocument(
                                new XElement(_xDocumentRoot, new XElement(_xElementReferences))
                                );
            }

            // Add/Update/Delete any references to assemblies
            var referenceRoot = settings.Root.Element(_xElementReferences);
            if (referenceRoot != null)
            {
                // Delete the old reference so we can re-add them
                referenceRoot.DescendantNodes().Remove();
            }
            else
            {
                settings.Root.AddFirst(new XElement(_xElementReferences));
                referenceRoot = settings.Root.Element(_xElementReferences);
            }

            if (referenceRoot != null)
            {
                // Write out the paths.  Reverse the list so it is in the same order as the list we passed in, due to the AddFirst
                foreach (ExternalReference reference in references)
                {
                    if (!String.IsNullOrWhiteSpace(reference.Path))
                    {
                        logger.Info("Adding reference {0} to setting file", reference.Path);
                        referenceRoot.Add(new XElement(_xElementReference, reference.Path));
                    }
                }
            }
            // Save the existing or new settings
            SaveSettingsToStorage(settings);
        }

        /// <summary>
        /// If there have been changes to the settings, send a message to all listeners with the new settings
        /// </summary>
        public void BroadcastChange()
        {
            if (_isSettingsDirty)
            {
                // Get the options from disk and create a message with the updated values
                var message = new ApplicationOptionsChangedMessage();
                message.NewSettings = GetOptions();

                // Send the message to any listener
                Messenger.Default.Send<ApplicationOptionsChangedMessage>(message);

                // Reset the flag so new changes will re-broadcast
                _isSettingsDirty = false;
            }
        }

        /// <summary>
        /// Gets the existing persisted settings from isolated storage.
        /// </summary>
        /// <param name="storageFileName">The name of the file used to store our settings.</param>
        /// <returns>
        /// Settings or null if nothing has been saved
        /// </returns>
        /// <remarks>
        /// Tried to follow the best practice, but had locking issues.  So put the using blocks back based on this http://stackoverflow.com/questions/3831676/ca2202-how-to-solve-this-case
        /// </remarks>
        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        protected internal virtual XDocument GetSettingsFromStorage(string storageFileName)
        {
            Contract.Ensures(Contract.Result<XDocument>() == null || Contract.Result<XDocument>().ToString().Length > 0, "Empty settings returned");
            Contract.Assert(!string.IsNullOrEmpty(storageFileName), "StorageFileName is empty");

            XDocument doc = null;
            try
            {
                // First get the 'user-scoped' storage information location reference in the assembly
                using (var isolatedStorage = IsolatedStorageFile.GetUserStoreForAssembly())
                {
                    // See if we have the settings
                    if (isolatedStorage.FileExists(storageFileName))
                    {
                        // Create a stream reader object to read content from the isolated location
                        using (var stream = new IsolatedStorageFileStream(storageFileName, FileMode.Open, isolatedStorage))
                        {
                            if (stream.Length == 0)
                            {
                                // The file is blank, just clean it up
                                logger.Warn("Empty settings file.  Deleting it");
                                stream.Close();
                                isolatedStorage.DeleteFile(storageFileName);
                            }
                            else
                            {
                                try
                                {
                                    // Open the file, load it into the XML and return the settings
                                    using (var srReader = XmlReader.Create(stream))
                                    {
                                        doc = XDocument.Load(srReader);
                                    }
                                }
                                catch
                                {
                                    // If there is an issue with the settings file, just delete it and continue on
                                    logger.Warn("Corrupt settings file.  Deleting it");
                                    stream.Close();
                                    isolatedStorage.DeleteFile(storageFileName);
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Helper.HandleException(logger, "Error trying to get settings from isolated storage", ex);
            }

            logger.Trace("GetSettingsFromStorage:" + (doc == null ? "<Null>" : "\r\n" + doc.ToString()));

            if (doc == null || doc.ToString().Length <= 0)
                return null;

            return doc;
        }

        /// <summary>
        /// Saves the existing persisted settings to isolated storage.
        /// </summary>
        /// <param name="doc">The doc to save</param>
        /// <remarks>
        /// Tried to follow the best practice, but had locking issues.  So put the using blocks back based on this http://stackoverflow.com/questions/3831676/ca2202-how-to-solve-this-case
        /// </remarks>
        [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
        protected internal virtual void SaveSettingsToStorage(XDocument doc)
        {
            Contract.Requires(doc != null, "Tried to save null document to disk");
            logger.Trace("SaveSettingsToStorage:" + (doc == null ? "<Null>" : "\r\n" + doc.ToString()));

            try
            {
                // First get the 'user-scoped' storage information location reference in the assembly
                using (var isolatedStorage = IsolatedStorageFile.GetUserStoreForAssembly())
                {
                    // Create or overwrite the existing file
                    using (var stream = new IsolatedStorageFileStream(StorageFileName, FileMode.Create, FileAccess.Write, isolatedStorage))
                    {
                        using (var srWriter = XmlWriter.Create(stream))
                        {
                            // Write the settings to storage
                            doc.Save(srWriter);
                        }
                    }
                }
            }
            catch (System.Security.SecurityException sx)
            {
                Helper.HandleException(logger, "Security exception: Error trying to save settings to isolated storage", sx);
            }
        }

        /// <summary>
        /// Clears the existing settings from storage
        /// </summary>
        public void ClearExistingSettings()
        {
            if (String.IsNullOrEmpty(StorageFileName))
                return;

            // First get the 'user-scoped' storage information location reference in the assembly
            using (var isolatedStorage = IsolatedStorageFile.GetUserStoreForAssembly())
            {
                if (isolatedStorage.FileExists(StorageFileName))
                    isolatedStorage.DeleteFile(StorageFileName);
            }
        }

        /// <summary>
        /// Return the connection string, which is stored in settings
        /// </summary>
        public string GetConnectionString
        {
            get
            {
                return "data source=localhost\\sqlExpress; initial catalog=ASPState;trusted_connection=true";
            }
        }

        /// <summary>
        /// The name of the file used to store our settings
        /// </summary>
        /// <remarks>Unit tests can override this to change where we are saving it</remarks>
        internal virtual string StorageFileName { get; set; }

    }
}
